﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ERF;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using FirstPerson.Objects;

namespace FirstPerson
{
    /// <summary>
    /// Represents the visual game world, which includes the terrain and the skybox.
    /// </summary>
    class GameWorld : GameObject, ERF.Physics.IHeightmapScene
    {
        private Map map;
        private Microsoft.Xna.Framework.Plane[] planes;
        private Skybox skybox;
        private Terrain terrain;
        private bool isDisposed;
        private float staticFriction = 0.3f;

        /// <summary>
        /// Creates a new instance of GameWorld.
        /// </summary>
        /// <param name="game">The game this instance belongs to.</param>
        /// <param name="map">The map that this GameWorld should encapsulate.</param>
        public GameWorld(ERFGame game, Map map)
            : base(game)
        {
            this.map = map;

            this.terrain = this.map.Terrain;
            this.skybox = this.map.Skybox;

            this.Children.Add(this.skybox);
            this.Children.Add(this.terrain);
            this.CreatePlanes();
        }

        /// <summary>
        /// Creates the custom collision planes for this world.
        /// </summary>
        private void CreatePlanes()
        {
            this.planes = new Microsoft.Xna.Framework.Plane[4];
            planes[0] = new Microsoft.Xna.Framework.Plane(Vector3.UnitZ, this.terrain.Size.Y / 2f);
            planes[1] = new Microsoft.Xna.Framework.Plane(new Vector3(0, 0, -1), (this.terrain.Size.Y / 2f) - 1);
            planes[2] = new Microsoft.Xna.Framework.Plane(Vector3.UnitX, this.terrain.Size.X / 2f);
            planes[3] = new Microsoft.Xna.Framework.Plane(new Vector3(-1, 0, 0), this.terrain.Size.X / 2f);
        }

        /// <summary>
        /// Gets the SkyBox.
        /// </summary>
        public Skybox SkyBox
        {
            get { return this.skybox; }
        }

        /// <summary>
        /// Gets the world terrain.
        /// </summary>
        public Terrain WorldTerrain
        {
            get { return this.terrain; }
        }

        public override void Update(Microsoft.Xna.Framework.GameTime gameTime, ref Microsoft.Xna.Framework.Matrix world)
        {
            if (this.isDisposed)
                return;

            base.UpdateWorld(ref world);
            base.Update(gameTime, ref base.world);
        }

        /// <summary>
        /// Performs bilinear interpolation to retrieve the exact height of the 
        /// terrain at the given world coordinate.
        /// </summary>
        /// <param name="x">X coordinate</param>
        /// <param name="z">Z coordinate</param>
        /// <returns>
        /// The height of the terrain at the given location.
        /// The value does not take any translations that has been made on the terrain into account.
        /// </returns>
        public float GetHeightFromXZ(float x, float z)
        {
            Vector2 offset = this.terrain.Offset;

            // Find the 4 vertices that encloses this coordinate.
            float offsetX = x - offset.X;
            float offssetZ = z - offset.Y;

            int topLeftIndex = (int)(Math.Floor(offsetX / this.terrain.HorizontalSpacing) + (Math.Floor(offssetZ / this.terrain.VerticalSpacing) * this.terrain.HorizontalVertices));
            int topRightIndex = topLeftIndex + 1;
            int bottomLeftIndex = topLeftIndex + this.terrain.HorizontalVertices;
            int bottomRightIndex = bottomLeftIndex + 1;
            float height;
            if (bottomRightIndex >= this.terrain.Vertices.Length || topLeftIndex < 0)
                return -1;
            Vector3 bottomLeftVector = this.terrain.Vertices[bottomLeftIndex].Position;
            Vector3 topRightVector = this.terrain.Vertices[topRightIndex].Position;
            Vector3 bottomRightVector = this.terrain.Vertices[bottomRightIndex].Position;
            Vector3 topLeftVector = this.terrain.Vertices[topLeftIndex].Position;

            // Calculate the x and z coordinate relative to the bounding square.
            float relativeX = (x - bottomLeftVector.X);
            float relativeZ = (bottomLeftVector.Z - z);

            // Normalize the x and z values to be within the range of 0 and 1.
            float xFactor = relativeX / (bottomRightVector.X - bottomLeftVector.X);
            float zFactor = relativeZ / (bottomRightVector.Z - topRightVector.Z);


            if (relativeX > relativeZ)
            { // if X > Z, we are in the bottom right triangle.
                height = bottomRightVector.Y;

                height += (1 - xFactor) * (bottomLeftVector.Y - bottomRightVector.Y);
                height += (zFactor) * (topRightVector.Y - bottomRightVector.Y);
            }
            else
            { // We are in the top left triangle (if the point is ON the diagonal, it'll count as being in this triangle aswell).
                height = topLeftVector.Y;

                height += xFactor * (topRightVector.Y - topLeftVector.Y);
                height += (1 - zFactor) * (bottomLeftVector.Y - topLeftVector.Y);
            }

            return height;
        }

        /// <summary>
        /// Returns the normal at a given position.
        /// </summary>
        /// <param name="x">X coordinate.</param>
        /// <param name="z">Z coordinate.</param>
        /// <returns>The normal at the given position.</returns>
        public Vector3 GetNormalAtXZ(float x, float z)
        {
            Vector3 normal = Vector3.Zero;
            Vector2 offset = this.terrain.Offset;

            // Find the 4 vertices that encloses this coordinate.
            float offsetX = x - offset.X;
            float offsetZ = z - offset.Y;

            // Check if we are out of bounds.
            if (offsetZ < 0 || offsetZ > this.terrain.Size.Y || offsetX < 0 || offsetX > this.terrain.Size.X)
            {
                return normal;
            }

            //int topLeftIndex = (int)(Math.Truncate(offsetX / this.terrain.HorizontalSpacing) + (Math.Truncate(offssetZ / this.terrain.VerticalSpacing) * this.terrain.HorizontalVertices));
            int topLeftIndex = (int)(Math.Floor(offsetX / this.terrain.HorizontalSpacing) + (Math.Floor(offsetZ / this.terrain.VerticalSpacing) * this.terrain.HorizontalVertices));
            int topRightIndex = topLeftIndex + 1;
            int bottomLeftIndex = topLeftIndex + this.terrain.HorizontalVertices;
            int bottomRightIndex = bottomLeftIndex + 1;

            Vector3 bottomLeftVector = this.terrain.Vertices[bottomLeftIndex].Position;
            Vector3 topRightVector = this.terrain.Vertices[topRightIndex].Position;
            Vector3 bottomRightVector = this.terrain.Vertices[bottomRightIndex].Position;
            Vector3 topLeftVector = this.terrain.Vertices[topLeftIndex].Position;

            // Calculate the x and z coordinate relative to the bounding square.
            float relativeX = (x - bottomLeftVector.X);
            float relativeZ = (bottomLeftVector.Z - z);

            if (relativeX > relativeZ)
            { // if X > Z, we are in the bottom right triangle.
                Vector3 side1 = bottomRightVector - bottomLeftVector;
                Vector3 side2 = topRightVector - bottomRightVector;
                Vector3.Cross(ref side1, ref side2, out normal);
                normal.Normalize();
            }
            else
            { // We are in the top left triangle (if the point is ON the diagonal, it'll count as being in this triangle aswell).
                Vector3 side1 = topRightVector - topLeftVector;
                Vector3 side2 = topLeftVector - bottomLeftVector;
                Vector3.Cross(ref side1, ref side2, out normal);
                normal.Normalize();
            }
            return normal;
        }

        public override void Dispose()
        {
            this.terrain.Dispose();
            this.terrain = null;
            this.skybox.Dispose();
            this.skybox = null;

            this.isDisposed = true;
        }

        /// <summary>
        /// Returns the Planes defining the boundaries of the world.
        /// </summary>
        /// <param name="planes"></param>
        public void GetCollisionPlanes(out Microsoft.Xna.Framework.Plane[] planes)
        {
            planes = this.planes;
        }


        #region IHeightmapScene Members


        public float StaticFrictionCoefficient
        {
            get { return this.staticFriction; }
        }

        #endregion
    }
}
